<#include "license.ftl">
<@license/>
<#assign object = doc.object>
package ${object.@package}.model.base;

<#if object.attributes.html[0]??>
import redora.util.HtmlSanitizer;
</#if>
<#list doc["/object/attributes/enum[@scope='global']"] as enum>
import ${object.@package}.model.enums.${enum.@class};
</#list>

import redora.exceptions.*;
import ${object.@package}.service.*;

<#if object.attributes.set[0]??>
import redora.set.*;
</#if>
import java.sql.ResultSet;
import java.util.Date;
import java.util.EnumMap;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.builder.HashCodeBuilder;

import redora.api.*;
<#if object.attributes.date[0]?? || object.attributes.datetime[0]??>
import java.text.ParseException;
import static redora.db.SQLParameter.yyyyMMdd;
import static redora.db.SQLParameter.yyyyMMddHHMMSS;
</#if>
import java.util.logging.Level;
import java.util.logging.Logger;
import ${object.@package}.model.*;
import ${object.@package}.model.fields.${object.@name}Fields;

import static redora.api.fetch.Page.ALL_TABLE;
import static redora.api.fetch.Page.ALL_LIST;

<#if object.description[0]??>
/** 
 * ${object.description}
 * @author Redora (www.redora.net)
*/
</#if>
public class ${object.@name}Base implements <#if object.sorted == "true">Sorted</#if>Persistable<#if object.interface[0]??>, ${object.interface}</#if> {
    private static final transient Logger l = Logger.getLogger("${object.@package}.model.${object.@name}Base");
    
    /** How deep should the isDirty() check go. @see #isDirty() */
    public static final int MAX_DIRTY_LEVEL = 7;
    public Long id;
    @Override
    public Long getId() {
        return id;
    }
    public void setId(@org.jetbrains.annotations.NotNull Long id) {
        if (this.id != null) throw new IllegalArgumentException("It is not allowed to modify the id");
        this.id = id;
    }
    public Date creationDate;
    public Date getCreationDate() {
        assert fetchScope != redora.api.fetch.Scope.List : "This object is fetched as Scope.List. Fetch the object with Table or Form scope instead.";
        return this.creationDate;
    }
    public Date updateDate;
    public Date getUpdateDate() {
        assert fetchScope != redora.api.fetch.Scope.List : "This object is fetched as Scope.List. Fetch the object with Table or Form scope instead.";
        return this.updateDate;
    }
<#list doc["/object/attributes/enum[@scope='local']"] as att>
    <#if att.description[0]??>
    /** ${att.description} */
    </#if>
    public enum ${att.@class} {
    <#list att.element as value>
        <#if value.@description[0]??>
        /** ${value.@description} */
        </#if>
        ${value.@name}<#if value_has_next>,<#else>;</#if>
    </#list>
        /** @return Null, or the result of valueOf() */
        public static ${att.@class} valueOfNullSafe(String value) {
            if (value != null) {
                return valueOf(value);
            }
            return null;
        }
    }
</#list>

<#list object.formScope?children as att><#if att?node_type == "element">
    <#if att.@notnull == "true" && att.@default[0]??>@org.jetbrains.annotations.NotNull</#if>
    public ${att.@className} ${att.@fieldName};
    </#if>
    <#if att?node_name == "object">
    /** @return true if ${att.@fieldName} does not need to be (lazy) fetched from DB anymore. */
    public boolean ${att.@fieldName}IsRetrieved() {
        return ${att.@fieldName} != null || ${att.@fieldName}Id == null || ${att.@fieldName}Id.equals(-1L);
    }
</#if></#list>

<#list object.attributes.set as att>
    protected final ${att.@className} ${att.@fieldName} = new Persistable<#if att.@multiplicity == "n-to-m" && (att.@sorted == "both" || att.@sorted == "them") || att.@sorted == "true">Sorted</#if>ArrayList<${att.@class}>(<#if att.@multiplicity == "n-to-m" && (att.@sorted == "both" || att.@sorted == "them")>true<#elseif att.@sorted == "true">false</#if>);
    /** @return true if ${att.@fieldName} does not need to be (lazy) fetched from DB anymore. */
    public boolean ${att.@fieldName}IsRetrieved() {
        return ${att.@fieldName}.isRetrieved();
    }
</#list>

    public final Map<${object.@name}Fields, Object> dirty = new EnumMap<${object.@name}Fields, Object>(${object.@name}Fields.class);
    public boolean isNew;
    /** How is this object retrieved. By default (if for example the object is new) it's scope is assumed as Form. */
    public redora.api.fetch.Scope fetchScope;

    /**
    * This method will set default values for:<br>
<#list doc["/object/attributes/*[@default]"] as att>
    * ${att.@fieldName} - ${att.@default};<br>
</#list>
    */
    protected ${object.@name}Base() {
        super();
<#list doc["/object/attributes/*[@default]"] as att>
    <#if att?node_name == "boolean">
        ${att.@fieldName} = Boolean.${att.@default?upper_case};
    <#elseif att?node_name == "string">
        ${att.@fieldName} = "${att.@default}";
    <#elseif att?node_name == "integer" || att?node_name == "long" || att?node_name == "double">
        ${att.@fieldName} = ${att.@default}<#if att?node_name == "long">L</#if>;
    <#elseif att?node_name == "enum">
        ${att.@fieldName} = ${att.@className}.${att.@default};
    <#else>
        error - undefined default ${att?node_name}
    </#if>
</#list>
        isNew = true;
        fetchScope = redora.api.fetch.Scope.Form;

<#list object.attributes.set as att>
        ${att.@fieldName}.reset();
</#list>
    }

    /**
    * Initializes ${object.@name} using a record in a ResultSet.
    * @param rs (Mandatory) ResultSet object using the field set defined in ${object.@name}SQLBase pointing to the record you want to serialize.
    * @param offset (Mandatory) Usually 0, but if the field set is more then this object's field set you must set the offset.
    * @param scope (Mandatory) The scope of the executed query, like Scope.Table of Scope.Form.
    */
    public ${object.@name}Base(@org.jetbrains.annotations.NotNull ResultSet rs, int offset
                    , @org.jetbrains.annotations.NotNull redora.api.fetch.Scope scope) throws CopyException {
        super();
        ${object.@name}Util.copy(rs, offset, scope, this);
        isNew = false;
        dirty.clear();
    }

<#list object.formScope?children as att>
    <#if att.description[0]??>
    /**
    * ${att.description}
    */
    </#if>
    public void set${att.@fieldName?cap_first}(@org.jetbrains.annotations.<#if att.@notnull == "true">NotNull<#else>Nullable</#if> ${att.@className} ${att.@fieldName}) <#if att.@lazy == "true" && att?node_name == "string">throws LazyException <#elseif att?node_name == "html">throws LazyException, FieldException</#if>{
        assert fetchScope != redora.api.fetch.Scope.List : "Modification is not allowed, this object is fetched as Scope.List and cannot be persisted. Fetch the object with Table or Form scope instead.";
    <#if att?node_name == "date" || att?node_name == "datetime">
        String stripped = null;
        //strip (nano) seconds - hours to ${att?node_name} level
        if (${att.@fieldName} != null) {
            stripped = yyyyMMdd<#if att?node_name == "datetime">HHMMSS</#if>.format(${att.@fieldName});
        }
        if ((this.${att.@fieldName} == null ? null : yyyyMMdd<#if att?node_name == "datetime">HHMMSS</#if>.format(this.${att.@fieldName})) != stripped) {
            if (!dirty.containsKey(${object.@name}Fields.${att.@fieldName})) {
                dirty.put(${object.@name}Fields.${att.@fieldName}, this.${att.@fieldName});
      	     }
            if (stripped == null) {
                this.${att.@fieldName} = null;
            } else {
                try {
                    this.${att.@fieldName} = yyyyMMdd<#if att?node_name == "datetime">HHMMSS</#if>.parse(stripped);
                } catch (ParseException e) {
                    l.log(Level.SEVERE, "Did not expect this would not parse to date " + stripped, e);
                    throw new RuntimeException("Did not expect this would not parse to date " + stripped, e);
                }
            }
        }
    }
    <#elseif att?node_name == "object">
        <#if att.@pigsear == "true">
        assert !this.equals(${att.@fieldName}) : "Circular reference " + ${att.@fieldName};
        </#if>
        if (!dirty.containsKey(${object.@name}Fields.${att.@fieldName}Id)) {
            if (${att.@fieldName}Id == null ? ${att.@fieldName} != null : ${att.@fieldName} == null || !${att.@fieldName}Id.equals(${att.@fieldName}.getId())) {
                dirty.put(${object.@name}Fields.${att.@fieldName}Id, this.${att.@fieldName}Id);
            }
        }
        this.${att.@fieldName} = ${att.@fieldName};
        if (this.${att.@fieldName} != null) {
            this.${att.@fieldName}Id = ${att.@fieldName}.getId();
        } else {
            this.${att.@fieldName}Id = null;
        }
    }
    <#elseif att?node_name == "html">
        if (${att.@fieldName} != null) {
            String _cleanHtml = HtmlSanitizer.clean("${att.@policy[0]!"antisamy.xml"}", ${att.@fieldName});
            if (!_cleanHtml.equals(this.${att.@fieldName})) {
                if (!dirty.containsKey(${object.@name}Fields.${att.@fieldName})) {
                    dirty.put(${object.@name}Fields.${att.@fieldName}, get${att.@fieldName?cap_first}());
                }
                this.${att.@fieldName} = _cleanHtml;
            }
        } else if (this.${att.@fieldName} != null) {
            if (!dirty.containsKey(${object.@name}Fields.${att.@fieldName})) {
                dirty.put(${object.@name}Fields.${att.@fieldName}, this.${att.@fieldName});
            }
            this.${att.@fieldName} = null;
        }
    }
    <#elseif att?node_type == "element">
        if ((${att.@fieldName} == null ? this.${att.@fieldName} != null : !${att.@fieldName}.equals(this.${att.@fieldName}))
                && !dirty.containsKey(${object.@name}Fields.${att.@fieldName})) {
            dirty.put(${object.@name}Fields.${att.@fieldName}, this.${att.@fieldName});
        }
        this.${att.@fieldName} = ${att.@fieldName};
    }
    </#if>

    <#if att.description[0]??>
    /**
    * ${att.description}
    */
    </#if>
    public ${att.@className} get${att.@fieldName?cap_first}() <#if att.@lazy == "true">throws LazyException </#if>{
    <#if att.@list == "false">
        assert fetchScope != redora.api.fetch.Scope.List : "This object is fetched as Scope.List. Fetch the object with Table or Form scope instead.";
    </#if>
    <#if att.@lazy == "true">
        <#if att?node_name == "object">
        if (${att.@fieldName}Id != null && ${att.@fieldName} == null) {
        <#else>
        if (fetchScope == redora.api.fetch.Scope.Table) {
        </#if>
            fetchLazy();
        }
    </#if>
        return ${att.@fieldName};
    }
</#list>

<#list object.attributes.set as att>
    /**
    <#if att.description[0]??>
    * ${att.description}
    </#if>
    <#if att.@sorted == "true">
    * <br>
    * This relationship is sortable, use the moveTo() function like get${att.@fieldName?cap_first}().moveTo(object, position);
    </#if>
    * <br>
    * If you wish to add a ${att.@class} to this list, do something like: get${att.@class}s().add(${att.@class});.
    * All the Collection methods like add, remove, addAll and removeAll are supported.
    * When you add or update ${att.@plural} to this set, they will be persisted together with ${object.@name}.
    * @return Empty or filled list.
    */
    @org.jetbrains.annotations.NotNull
    public ${att.@className} get${att.@fieldName?cap_first}() throws QueryException {
        if (!${att.@fieldName}.isRetrieved()) {
            ${att.@class}Service service = null;
            try {
                service = ServiceFactory.${att.@class?uncap_first}Service();
                ${att.@fieldName}.addAll(service.finder(${object.@package}.sql.base.${att.@class}SQLBase.DefaultFinder.FindBy${att.@myName?cap_first}Id, id, fetchScope == redora.api.fetch.Scope.List ? ALL_LIST : ALL_TABLE));
            } catch (RedoraException e) {
                throw new QueryException("Failed to retrieve ${att.@fieldName} for " + this.toString(), e);
            } finally {
                ServiceFactory.close(service);
            }
            ${att.@fieldName}.reset();
        }

        return ${att.@fieldName};
    }
</#list>

    /**
    * Checks if any of the attributes have changed. Also the related children and objects are checked.
    * If you want to avoid the related object dirty check, use isDirty(${object.@name}.MAX_DIRTY_LEVEL) instead.
    * @return true if any of the attributes of ${object.@name} have changed
    */
    public boolean isDirty() {
        return isDirty(new HashSet<Persistable>());
    }

    /**
    * {@inheritDoc}
    * @see #isDirty()
    * @param ignore (Optional) List of already dirty checked objects to avoid circular checking.
    * @return true is somewhere something has changed
    */
    @Override
    public boolean isDirty(@org.jetbrains.annotations.NotNull Set<Persistable> ignore) {
        if (!dirty.isEmpty()) {
            return true;
        }
    <#if doc["/object/attributes/set"][0]?? || doc["/object/attributes/object"][0]??>
        try {
        <#list doc["/object/attributes/set"] as att>
            if (${att.@fieldName}IsRetrieved() && get${att.@fieldName?cap_first}().isDirty(ignore)) {
                return true;
            }
        </#list>
        <#list doc["/object/attributes/object"] as att>
            if (${att.@fieldName}IsRetrieved() && get${att.@fieldName?cap_first}() != null && ignore.add(get${att.@fieldName?cap_first}()) && get${att.@fieldName?cap_first}().isDirty(ignore)) {
                return true;
            }
        </#list>
        } catch (RedoraException e) {
            l.log(Level.SEVERE, "This exception can't happen because i should not perform any kind of DB related activities", e);
        }
    </#if>
        return false;
    }

    @Override
    /** Checks primarily on Id, if there is no Id, it will try hashCode. */
    public boolean equals(Object that) {
        if (this == that)
            return true;
        if (that instanceof ${object.@name})
            return equals((${object.@name})that);
        return false;
    }

    /** Checks primarily on Id, if there is no Id, it will try hashCode. */
    public boolean equals(${object.@name} that) {
        if (id != null)
            return that.id != null && id.longValue() == that.id.longValue();
        else
            return that.id == null && hashCode() == that.hashCode();
    }

    /**
    * The hashCode is computed on the Id which is as fast as it can get. If there is
    * no Id (new object), the hash code is calculated on all the other fields.
    * @return the hash code of Id, or, when null, a hash code created by all the other fields.
    */
    @Override
    public int hashCode() {
        if (id != null) {
            return id.hashCode();
        }
        return new HashCodeBuilder(<#if object.@sequence?number % 2 == 0>${object.@sequence?number + 97}<#else>${object.@sequence}</#if>, 37)
        <#list object.formScope?children as att>
            <#if att?node_name != "object">
            .append(${att.@fieldName})
            </#if>
        </#list>    .toHashCode();
    }

    /**
    * Provides info from following fields:
<#if object.listScope[0]??>
    <#list object.listScope?children as att>
        <#if att?node_name != "object">
    * ${att.@fieldName}
        </#if>
    </#list>
</#if>
    * and id
    */
    @Override
    public String toString() {
        StringBuilder retVal = new StringBuilder();
<#if object.listScope[0]??>
    <#list object.listScope?children as att>
        <#if att?node_name != "object">
        retVal.append(${att.@fieldName}).append(" ");
        </#if>
    </#list>
</#if>
        retVal.append('(').append(id).append(')');
        return retVal.toString();
    }

    /**
    * Creates a new ${object.@name} with all the same values, also related children are cloned.
    * When you persist, only the Id('s) will differ.
    * <br>
    * Parent objects, will be copied, 1-to-n children will be cloned, n-to-m children will
    * be copied. In fact, everything will be copied, only children will be cloned.
    * <br>
    * The clone method is somewhat potent because it also clones related objects.
    * It however restrains itself by cloning until the MAX_DIRTY_LEVEL and direct
    * pigsears (1-to-n and n-to-m) are resolved. Indirect pigsears (relations that
    * use an intermediate object) are not resolved and can lead to runaway cloning.
    * <br>
    * id, creationDate and updateDate are not copied.
    * @see #MAX_DIRTY_LEVEL
    * @return the cloned ${object.@name} or null when there was a LazyException.
    */
    @Override
    @SuppressWarnings({"CloneDoesntCallSuperClone"})
    public ${object.@name} clone() {
        return clone(1);
    }

    /**
    * Creates a new ${object.@name} with all the same values, also related children are cloned.
    * When you persist, only the Id('s) will differ.
    * This method is used primarily to implement the clone() method.
    * @see #clone()
    * @param depth The depth of the cloning process into a string of relations.
    * @return the cloned ${object.@name} or null when there was a LazyException.
    */
    @Override
    public ${object.@name} clone(int depth) {
        ${object.@name} retVal = new ${object.@name}();
        retVal.cloneMasterId = getId();
        try {
<#list object.attributes?children as att>
     <#if att?node_type == "element">
        <#if att?node_name == "set">
            <#if att.@multiplicity == "n-to-m">
            retVal.get${att.@fieldName?cap_first}().addAll(get${att.@fieldName?cap_first}());
            <#else>
            if (depth < MAX_DIRTY_LEVEL) {
                retVal.get${att.@fieldName?cap_first}().addAll(get${att.@fieldName?cap_first}().clone(depth + 1));
            }
            </#if>
        <#elseif att?node_name == "object">
            //Only copy object relations on the first level, if you need more, then override this behavior in ${object.@name}.java.
            if (depth == 1 && get${att.@fieldName?cap_first}() != null) {
                retVal.set${att.@fieldName?cap_first}(get${att.@fieldName?cap_first}().clone(MAX_DIRTY_LEVEL));
            }
        <#elseif (att?node_name != "long" || !att.@parentClass[0]??)>
            retVal.set${att.@fieldName?cap_first}(get${att.@fieldName?cap_first}());
        </#if>
    </#if>
</#list>
        } catch (Exception e) {
            l.log(Level.SEVERE, "Failed to clone this ${object.@name} " + getId(), e);
            retVal = null;
        }
        return retVal;
    }
    /** When cloned, the master's id is kept here. This id is not persisted.*/
    public Long cloneMasterId;

<#if object.hasLazy == "true">
    protected void fetchLazy() throws LazyException {
        //Set fetchScope at the beginning, otherwise you might get an infinite loop with get/set thinking it is still lazy
        fetchScope = redora.api.fetch.Scope.Form;

        try {
    <#if object.lazyScope[0]??>
            if (!isNew) {
                ${object.@name}Service service = ServiceFactory.${object.@name?uncap_first}Service();
                Map<String, Object> _lazy = service.fetchLazy(id);
                ServiceFactory.close(service);
        <#list doc["/object/lazyScope/*"] as att>
            <#if att?node_type == "element">
                if (_lazy.get(${object.@name}Fields.${att.@fieldName}.name()) != null) {
                    ${att.@fieldName} = (${att.@className})_lazy.get(${object.@name}Fields.${att.@fieldName}.name());
                }
            </#if>
        </#list>
            }
    </#if>
    <#list doc["/object/attributes/object[@lazy='true']"] as att>
            if (${att.@fieldName}Id != null) {
                ${att.@className}Service service = ServiceFactory.${att.@className?uncap_first}Service();
                try {
                    ${att.@fieldName} = service.findById(${att.@fieldName}Id, redora.api.fetch.Scope.Table);
                } catch (RedoraException e) {
                    throw new LazyException("Can retrieve ${att.@fieldName} " + ${att.@fieldName}Id, e);
                } finally {
                    ServiceFactory.close(service);
                }
            }
    </#list>
        } catch (ConnectException e) {
            throw new LazyException("Failed to get a connection to retrieve lazy fetched attributes for ${object.@name} " + id, e);
        }
    }
</#if>

<#if object.sorted == "true">
    @Override
    public int compareTo(Object o) {
        return getSortOrder().compareTo(((${object.@name})o).getSortOrder());
    }
</#if>
}
